package org.melanesia.conf;

import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.melanesia.beans.validation.PropertyValidationData;
import org.melanesia.dom.Node;

/**
 * Configuration repository.
 *
 * @author marcin.kielar
 */
public abstract class Config {

    /** Logger. */
    private static Logger logger = Logger.getLogger(Config.class.getName());

    /** Configuration file name. */
    private static final String CONFIG_FILE_NAME = "/melanesia-config.xml";

    /** Property validation configuration. */
    private static final Map<String, Map<String, PropertyValidationData>> PROPERTY_VALIDATION_DATA =
            new HashMap<String, Map<String, PropertyValidationData>>();

    /** Default value for composite property separator character. */
    private static final String DEFAULT_COMPOSITE_PROPERTY_SEPARATOR = ".";

    /** Default value for reflection metadata cache size. */
    private static final int DEFAULT_REFLECTION_DATA_CACHE_SIZE = 10000;

    /**
     * Default value for {@link org.melanesia.sql.Query} fetch size, as
     * described in {@link java.sql.Statement#setFetchSize(int)}.
     */
    private static final int DEFAULT_QUERY_FETCH_SIZE = 1000;

    /** Debug mode flag. */
    private static Boolean debugMode = null;

    /**
     * Should autoboxing aware search be enabled in {@link org.melanesia.converters.TypeConverter#findJavaToJavaConverter(Class, Class)}
     * and {@link org.melanesia.converters.TypeConverter#findJavaToJdbcConverter(Class)}.
     */
    private static Boolean typeConverterAutoboxingSearch = false;

    /**
     * Should super class inspection be enabled in {@link org.melanesia.converters.TypeConverter#findJavaToJavaConverter(Class, Class)}
     * and {@link org.melanesia.converters.TypeConverter#findJavaToJdbcConverter(Class)}.
     */
    private static Boolean typeConverterSuperClassSearch = false;
    
    /** Bean validation enabled flag. */
    private static Boolean beanValidationEnabled = true;

    /** Composite property separator character. */
    private static String compositePropertySeparator = DEFAULT_COMPOSITE_PROPERTY_SEPARATOR;

    /** Reflection metadata cache size. */
    private static int reflectionDataCacheSize = DEFAULT_REFLECTION_DATA_CACHE_SIZE;

    /**
     * {@link org.melanesia.sql.Query} fetch size, as described in
     * {@link java.sql.Statement#setFetchSize(int)}.
     */
    private static int queryFetchSize = DEFAULT_QUERY_FETCH_SIZE;

    static {

        try {
            InputStream inputStream = Config.class.getResourceAsStream(CONFIG_FILE_NAME);
            if (inputStream != null) {

                Node root = new Node(inputStream);

                for (Node node : root.getChildren()) {
                    String nodeName = node.getNodeName();

                    if ("composite-property-separator".equals(nodeName)) {
                        bindCompositePropertySeparator(node);
                    } else if ("reflection-data-cache-size".equals(nodeName)) {
                        bindReflectionDataCacheSize(node);
                    } else if ("query-fetch-size".equals(nodeName)) {
                        bindQueryFetchSize(node);
                    } else if ("bean-validation-enabled".equals(nodeName)) {
                        bindBeanValidationEnabled(node);
                    } else if ("bean-validation".equals(nodeName)) {
                        bindBeanValidation(node);
                    } else if ("debug-mode".equals(nodeName)) {
                        bindDebugMode(node);
                    } else if ("type-converter-autoboxing-search".equals(nodeName)) {
                        bindTypeConverterAutoboxingSearch(node);
	                } else if ("type-converter-superclass-search".equals(nodeName)) {
	                    bindTypeConverterSuperclassSearch(node);
	                }

                }

                bindDebugModeSystemProperty();

                logger.info("Config file: \"" + CONFIG_FILE_NAME + "\" loaded successfully.");

            } else {
                logger.warning("Config file: \"" + CONFIG_FILE_NAME + "\" not found. Assumming defaults.");
            }
        } catch (Throwable t) {
            logger.log(Level.WARNING, "Error reading configuration file \"" + CONFIG_FILE_NAME + "\". Assuming defaults.", t);
        }
    }

    /**
     * Binds XML to related configuration property.
     *
     * @param node
     *            xml node
     */
    private static void bindCompositePropertySeparator(final Node node) {
        compositePropertySeparator = node.getTextContent();
    }

    /**
     * Binds XML to related configuration property.
     *
     * @param node
     *            xml node
     */
    private static void bindReflectionDataCacheSize(final Node node) {
        reflectionDataCacheSize = Integer.parseInt(node.getTextContent());
    }

    /**
     * Binds XML to related configuration property.
     *
     * @param node
     *            xml node
     */
    private static void bindQueryFetchSize(final Node node) {
        queryFetchSize = Integer.parseInt(node.getTextContent());
    }

    /**
     * Binds XML to related configuration property.
     *
     * @param node
     *            xml node
     */
    private static void bindBeanValidationEnabled(final Node node) {
        beanValidationEnabled = Boolean.parseBoolean(node.getTextContent());
    }

    /**
     * Binds XML to related configuration property.
     *
     * @param node
     *            xml node
     */
    private static void bindBeanValidation(final Node node) {

        for (Node beanNode : node.getChildren("bean")) {

            String className = beanNode.getAttribute("class");
            for (Node propertyNode : beanNode.getChildren("property")) {

                String propertyName = propertyNode.getAttribute("name");
                Boolean nullable = Boolean.parseBoolean(propertyNode.getAttribute("nullable"));
                String validatorClass = propertyNode.getAttribute("validator");

                addBeanValidation(className, propertyName, nullable, validatorClass);
            }
        }
    }

    /**
     * Binds XML to related configuration property.
     *
     * @param node
     *            xml node
     */
    private static void bindDebugMode(final Node node) {
        debugMode = Boolean.parseBoolean(node.getTextContent());
    }

    /**
     * Binds XML to related configuration property.
     *
     * @param node
     *            xml node
     */
    private static void bindTypeConverterAutoboxingSearch(final Node node) {
        typeConverterAutoboxingSearch = Boolean.parseBoolean(node.getTextContent());
    }
    
    /**
     * Binds XML to related configuration property.
     *
     * @param node
     *            xml node
     */
    private static void bindTypeConverterSuperclassSearch(final Node node) {
        typeConverterSuperClassSearch = Boolean.parseBoolean(node.getTextContent());
    }
    
    /**
     * Binds system property {@code org.melanesia.conf.Config.debugMode} to
     * related configuration property.
     */
    private static void bindDebugModeSystemProperty() {

        if (System.getProperties().containsKey("org.melanesia.conf.Config.debugMode")) {

            Boolean newDebugMode = Boolean.parseBoolean(System.getProperty("org.melanesia.conf.Config.debugMode"));

            if (debugMode != null && newDebugMode != debugMode) {
                // debug mode was set earlier and is different - log warning
                logger.warning("Debug mode defined differently in system property (" + newDebugMode + ") and configuration file ("
                        + debugMode + "). Using system property.");
            }

            debugMode = newDebugMode;
        }
    }

    /**
     * Stores bean validation properties for further use in
     * {@link org.melanesia.beans.validation.BeanValidator}.
     *
     * @param className
     *            class name
     * @param propertyName
     *            property name
     * @param nullable
     *            whether to validate property for null values
     * @param validatorClass
     *            custom validator class
     */
    private static void addBeanValidation(final String className, final String propertyName, 
            final Boolean nullable, final String validatorClass) {

        if (!PROPERTY_VALIDATION_DATA.containsKey(className)) {
            PROPERTY_VALIDATION_DATA.put(className, new HashMap<String, PropertyValidationData>());
        }

        PROPERTY_VALIDATION_DATA.get(className).put(propertyName, new PropertyValidationData(nullable, validatorClass));
    }

    /**
     * Returns debug mode setting.
     *
     * @return is debug mode enabled
     */
    public static boolean isDebugMode() {
        return debugMode;
    }

    /**
     * Returns the TypeConverter autoboxing search setting.
     *
     * @return true if autoboxing should be used when looking the converter up in the TypeConverter
     */
    public static boolean isTypeConverterAutoboxingSearch() {
        return typeConverterAutoboxingSearch;
    }

    /**
     * Returns the TypeConverter super class search setting.
     *
     * @return true if super class converters should be ispected when looking the converter up in the TypeConverter
     */
    public static boolean isTypeConverterSuperClassSearch() {
        return typeConverterSuperClassSearch;
    }
    
    /**
     * Returns composite property separator character.
     *
     * @return composite property separator character
     */
    public static String getCompositePropertySeparator() {
        return compositePropertySeparator;
    }

    /**
     * Returns reflection metadata cache size.
     *
     * @return reflection metadata cache size
     */
    public static int getReflectionDataCacheSize() {
        return reflectionDataCacheSize;
    }

    /**
     * Returns {@link org.melanesia.sql.Query} fetch size, as described in
     * {@link java.sql.Statement#setFetchSize(int)}.
     *
     * @return query fetch size
     */
    public static int getQueryFetchSize() {
        return queryFetchSize;
    }

    /**
     * Returns bean validation setting.
     *
     * @return bean validation setting
     */
    public static boolean isBeanValidationEnabled() {
        return beanValidationEnabled;
    }

    /**
     * Returns map of the validated properties and their validation metadata for
     * use in {@link org.melanesia.beans.validation.BeanValidator}.
     *
     * @param beanClass
     *            bean class
     * @return map of the validated properties and their validation metadata
     */
    public static Map<String, PropertyValidationData> getValidatedProperties(final Class<?> beanClass) {
        return PROPERTY_VALIDATION_DATA.get(beanClass.getName());
    }
}
